﻿/*
This project and all of its code is/are Copyright (C) 2017  Nils Hunter Alving

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace DynamicTypeBuilder
{
    internal class TypeFactoryHelper
    {
        MethodAttributes _get_setAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual;
        ModuleBuilder _modBuilder;
        BindingFlags _bindingFlags = BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance;
        string _outputAssemblyName;

        internal TypeFactoryHelper(ModuleBuilder modBuilder)
        {
            _modBuilder = modBuilder;
            _outputAssemblyName = modBuilder.Assembly.GetName().Name;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="interfaces"></param>
        internal void CreateTypesFromInterfacesInAssemblyModule(List<Type> interfaces)
        {
            interfaces.ForEach(i =>
            {
                var declaredMethods = i.GetMethods().Where(m => !m.IsSpecialName).ToList();
                var parentInterfaces = i.GetInterfaces().ToList();
                parentInterfaces.ForEach(pi => declaredMethods.AddRange(pi.GetMethods().Where(m =>! m.IsSpecialName)));

                if (declaredMethods.Any())
                {
                    throw new ArgumentException("Interfaces containing methods are not currently supported");
                }
                var typeBuilder = _modBuilder.DefineType($"{_outputAssemblyName}.{i.Name}", TypeAttributes.Public, typeof(object), new Type[] { i });
                if (!File.Exists(i.Assembly.ManifestModule.Name))
                {
                    throw new FileNotFoundException($"Could not locate file: {i.Assembly.FullName} in directory {Environment.CurrentDirectory}");
                }
                var declaredProperties = i.GetProperties(_bindingFlags).ToList();
                parentInterfaces.ForEach(pi => declaredProperties.AddRange(pi.GetProperties(_bindingFlags)));

                declaredProperties.ForEach(p =>
                {
                    var backingField = typeBuilder.DefineField($"_{p.Name}", p.PropertyType, FieldAttributes.Private);
                    var propBuilder = typeBuilder.DefineProperty(p.Name, PropertyAttributes.HasDefault, p.PropertyType, null);
                    if (p.GetMethod != null)
                    {
                        CreateGetMethod(p, typeBuilder, propBuilder, backingField);
                    }
                    if (p.SetMethod != null)
                    {
                        CreateSetMethod(p, typeBuilder, propBuilder, backingField);
                    }
                });
                typeBuilder.CreateType();
            });
        }

        private void CreateGetMethod(PropertyInfo property, TypeBuilder typeBuilder, PropertyBuilder propBuilder, FieldBuilder backingField)
        {
            var getMethodBuilder = typeBuilder.DefineMethod(property.GetMethod.Name, _get_setAttributes, property.PropertyType, Type.EmptyTypes);
            var ilGenerator = getMethodBuilder.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldfld, backingField);
            ilGenerator.Emit(OpCodes.Ret);
            propBuilder.SetGetMethod(getMethodBuilder);
        }

        private void CreateSetMethod(PropertyInfo property, TypeBuilder typeBuilder, PropertyBuilder propBuilder, FieldBuilder backingField)
        {
            var parameters = property.SetMethod.GetParameters().Select(prm => prm.ParameterType).ToArray();
            var setMethodBuilder = typeBuilder.DefineMethod(property.SetMethod.Name, _get_setAttributes, null, parameters);
            var ilGenerator = setMethodBuilder.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.Emit(OpCodes.Stfld, backingField);
            ilGenerator.Emit(OpCodes.Ret);
            propBuilder.SetSetMethod(setMethodBuilder);
        }
    }
}
